﻿#region Using Statements
using System;
using System.Collections.Generic;
using System.Diagnostics;
using DirectZZT.Shared.Core;
#endregion

// TODO: review for accuracy of original ZZT-oop!

namespace DirectZZT.Shared.Scripting.ScriptLanguages
{
    /// <summary>
    /// Implements the <see cref="IScript"/> interface to provide the logic for a ZZT-oop script.
    /// </summary>
    [Serializable()]
    [Export("ZZToop", Type = typeof(IScript))]
    public class ZZTOopScript : IScript
    {
        #region Static Fields

        private static Dictionary<string, Action<ZZTOopScript, string>> METHODSCACHE;

        #endregion

        #region Fields

        /// <summary>
        /// The cycle of the script.
        /// </summary>
        private int _cycle = Scripting.Script.DefaultCycle;
        /// <summary>
        /// Timer to schedule line-by-line execution for ZZT-oop scripts (only).
        /// </summary>
        private Stopwatch _zztoopTimer;
        private ScriptState _state;
        private IScriptHost _host;
        /// <summary>
        /// Instruction line pointer.
        /// </summary>
        private int _currentLinePointer = -1;
        private bool _isLocked;

        #endregion

        #region Constructors

        static ZZTOopScript()
        {
            METHODSCACHE = new Dictionary<string, Action<ZZTOopScript, string>>();
            METHODSCACHE.Add("IF", MIf);
            METHODSCACHE.Add("SEND", MSend);
            // global flags manipulators
            METHODSCACHE.Add("SET", MSet);
            METHODSCACHE.Add("CLEAR", MClear);
            // script manipulators
            METHODSCACHE.Add("CYCLE", MCycle);
            METHODSCACHE.Add("LOCK", MLock);
            METHODSCACHE.Add("UNLOCK", MUnlock);

            // game logic
            METHODSCACHE.Add("BECOME", MBecome);
            METHODSCACHE.Add("DIE", MDie);
        }

        /// <summary>
        /// Initializes a new instance of the ZZTOopScript class.
        /// </summary>
        public ZZTOopScript()
        {
            _state = ScriptState.Unstarted;
        }

        #endregion

        #region Methods

        /// <summary>
        /// Executes one line.
        /// </summary>
        /// <param name="i"></param>
        private void ExecuteLine(int i)
        {
            // TODO: set instruction pointer accurately!
            this.CurrentInstruction = i;

            // notify debugger
            ScriptDebugger.NotifyExecutionLineChanged(this, i);

            string line = this.Script[i].Trim();
            if (string.IsNullOrWhiteSpace(line))
            {
                // nothing to do
                return;
            }

            DoString(line);
        }

        private void DoString(string commandLine)
        {
            // get prefix (if any)
            char start = commandLine[0];
            string commands = commandLine.Substring(1, commandLine.Length - 1);
            switch (start)
            {
                case '@':
                    {
                        // give our parent a name
                        _host.Name = commands;
                    } break;
                case '\'':
                    {
                        // comment
                    } return;
                case '#':
                    {
                        // command
                        TryExecuteMethod(commands);
                    } break;
                case ':':
                    {
                        // label, ignore
                        // TODO: directly go to next line, don't wait for the cycle to complete!!
                    } break;
                case '/':
                    {
                        // attempt to move into direction
                    } break;
                case '?':
                    {
                        // attempt to move into direction
                    } break;
                default:
                    {
                        // not recognized line
                        Logging.GetLogging().LogFormat(LoggingMessageType.Trace, "Script", "ZZT-oop", "Line not recognized: '{0}'", commandLine);
                    } break;
            }
        }

        private void TryExecuteMethod(string line)
        {
            string methodName = line;
            if (line.Contains(" "))
            {
                methodName = line.Substring(0, line.IndexOf(" "));
                line = line.Remove(0, methodName.Length + 1);
            }
            else
            {

            }
            if (METHODSCACHE.ContainsKey(methodName))
            {
                METHODSCACHE[methodName](this, line);
            }
        }

        #endregion

        #region IScript Members

        /// <summary>
        /// The script.
        /// </summary>
        public string[] Script { get; set; }
        /// <summary>
        /// The offset in bytes of the code block to execute ZZT-OOP from. Only Objects and Scrolls can execute ZZT-OOP. 
        /// </summary>
        public int CurrentInstruction { get; set; }

        ScriptState IScript.State
        {
            get { return _state; }
        }

        void IScript.OnAttach(IScriptHost scriptHost)
        {
            _host = scriptHost;
            _state = ScriptState.Running;
        }

        void IScript.OnDetach(IScriptHost scriptHost)
        {
            _host = null;
            _state = ScriptState.Finished;
        }

        void IScript.Initialize()
        {

        }

        void IScript.Update()
        {
            if (_zztoopTimer == null)
            {
                _zztoopTimer = Stopwatch.StartNew();
            }

            // cycle?
            if (_zztoopTimer.ElapsedMilliseconds >= _cycle)
            {
                // go to next line?
                if (_currentLinePointer < this.Script.Length - 1)
                {
                    try
                    {
                        _currentLinePointer++;
                        ExecuteLine(_currentLinePointer);

                        _zztoopTimer.Restart();

                        if (_state.HasFlag(ScriptState.DEBUG_STEP))
                        {
                            // remove step
                            _state ^= ScriptState.DEBUG_STEP;
                            // just to be safe...
                            _state |= ScriptState.DEBUG_BREAK;

                            // notify debugger
                            ScriptDebugger.NotifyEvent(this, ScriptDebugger.EventType.Debug, "Step finished");
                        }
                    }
                    catch (Exception ex)
                    {
                        _state = ScriptState.Faulted;
                        ScriptDebugger.NotifyEvent(this, ScriptDebugger.EventType.Fault, "Script faulted", "Exception", ex.GetType().Name, ex.Message);
                    }
                }
                else
                {
                    // that's all folks
                    _state = ScriptState.Finished;
                    ScriptDebugger.NotifyEvent(this, ScriptDebugger.EventType.Info, "Script finished");
                }
            }
        }

        ScriptLanguage IScript.Language
        {
            get { return ScriptLanguage.ZZToop; }
        }

        void IScript.OnSend(string eventName)
        {
            GotoLabel(this, eventName);
        }

        void IScript.Stop()
        {
            _state = ScriptState.Finished;
        }

        #endregion

        #region Script methods

        /// <summary>
        /// Modifies the current line pointer to point to the line containing the given label.
        /// </summary>
        /// <param name="script"></param>
        /// <param name="label"></param>
        private static void GotoLabel(ZZTOopScript script, string label)
        {
            // first check if the script is locked, if so, ignore it
            if (script._isLocked)
            {
                // notify
                ScriptDebugger.NotifyEvent(script, ScriptDebugger.EventType.Trace, "Event sent to locked script [self]. Igoring event.", label);
            }
            else
            {

                for (int i = 0; i < script.Script.Length; i++)
                {
                    string line = script.Script[i];
                    if (line.StartsWith(":"))
                    {
                        if (line.Remove(0, 1) == label)
                        {
                            // found!
                            script._currentLinePointer = i;
                            return;
                        }
                    }
                }

                // notify
                ScriptDebugger.NotifyEvent(script, ScriptDebugger.EventType.Warning, "Label not found", label);
            }
        }

        private static void MBecome(ZZTOopScript script, string line)
        {
            // Syntax: 
            //    #BECOME [COLOR] ELEMENT

            string[] tokens = line.Split(' ');
            if (tokens.Length == 1 || tokens.Length == 2)
            {
                script._host.OnEvent("BECOME", tokens);
            }
            // invalid syntax
        }

        private static void MIf(ZZTOopScript script, string line)
        {
            // Syntax: 
            //    #IF [FLAG] THEN [LABEL]

            string[] tokens = line.Split(' ');
            if (tokens.Length < 3 || tokens[1] != "THEN")
            {
                // invalid syntax
                return;
            }

            string flag = tokens[0];
            // its easy here because zzt-oop only knows bools
            if (script._host.ScriptExecutor.GetFlag(flag) != null)
            {
                //string command = string.Join(" ", tokens, 2, tokens.Length - 2);

                GotoLabel(script, tokens[2]);
            }
        }

        private static void MSend(ZZTOopScript script, string line)
        {
            if (line.Contains(" "))
            {
                string[] tokens = line.Split(' ');
                switch (tokens[0].ToUpperInvariant())
                {
                    case "ALL":
                        {

                        } break;
                    case "OTHERS":
                        {

                        } break;
                    default:
                        ScriptDebugger.NotifyEvent(script, ScriptDebugger.EventType.Trace, "send", "invalid argument", tokens[0]);
                        break;
                }
            }
            else
            {
                string[] tokens = line.Split(':');
                if (tokens.Length == 1)
                {
                    // this command is targeted towards ourselves
                    GotoLabel(script, tokens[0]);
                }
                else if (tokens.Length == 2)
                {
                    // this is targetted towards an object or a set of objects
                    script._host.ScriptExecutor.SendEvent(tokens[0], "SEND", tokens[1]);
                }
                else
                {
                    ScriptDebugger.NotifyEvent(script, ScriptDebugger.EventType.Trace, "send", "invalid tokens");
                }
            }
        }

        private static void MSet(ZZTOopScript script, string line)
        {
            string flag = line;

            // set flag value
            if (script._host.ScriptExecutor.GetFlag(flag) == null)
            {
                if (script._host.ScriptExecutor.SetFlag(flag, null))
                {
                    // notify of successful flag set
                    ScriptDebugger.NotifyEvent(script, ScriptDebugger.EventType.Trace, "flag", "set", flag);
                }
                else
                {
                    // notify that setting was not possible because ZZT's 10-flag-limit was reached
                    ScriptDebugger.NotifyEvent(script, ScriptDebugger.EventType.Warning, "flag", "not set", "reached 10-flag limit for feature set ZZT!");
                }
            }
        }

        private static void MClear(ZZTOopScript script, string line)
        {
            // clear flag
            string flag = line;
            if (script._host.ScriptExecutor.GetFlag(flag) != null)
            {
                script._host.ScriptExecutor.RemoveFlag(flag);

                ScriptDebugger.NotifyEvent(script, ScriptDebugger.EventType.Trace, "flag", "clear", flag);
            }
        }

        private static void MCycle(ZZTOopScript script, string line)
        {
            // apply new cycle time
            int cycleTime = script._cycle;
            if (int.TryParse(line, out cycleTime))
            {
                script._cycle = cycleTime;

                ScriptDebugger.NotifyEvent(script, ScriptDebugger.EventType.Trace, "cycle", "set", cycleTime.ToString());
            }
        }

        private static void MLock(ZZTOopScript script, string line)
        {
            script._isLocked = true;
        }

        private static void MUnlock(ZZTOopScript script, string line)
        {
            script._isLocked = false;
        }

        private static void MDie(ZZTOopScript script, string line)
        {
            script._host.OnEvent("DIE");
            script._state = ScriptState.Finished;
        }

        #endregion

        #region IDebuggableScript Members

        string IDebuggableScript.DebugName
        {
            get { return _host.Name + "Script"; }
        }

        void IDebuggableScript.DebugBreak()
        {
            this._state |= ScriptState.DEBUG_BREAK;

            // notify debugger
            ScriptDebugger.NotifyEvent(this, ScriptDebugger.EventType.Debug, "Breaking script execution.");
        }

        void IDebuggableScript.DebugContinue()
        {
            this._state ^= ScriptState.DEBUG_BREAK;

            // notify debugger
            ScriptDebugger.NotifyEvent(this, ScriptDebugger.EventType.Debug, "Continuing script execution.");

            // if script execution has finished, start script over
            this._state = ScriptState.Running;
            this._state = ScriptState.Running;
            this._currentLinePointer = 0;
        }

        void IDebuggableScript.DebugStep()
        {
            this._state |= ScriptState.DEBUG_STEP;

            // notify debugger
            ScriptDebugger.NotifyEvent(this, ScriptDebugger.EventType.Debug, "Stepping through script");
        }

        void IDebuggableScript.DebugRestart()
        {
            _currentLinePointer = 0;
            _state = ScriptState.Running;

            ScriptDebugger.NotifyEvent(this, ScriptDebugger.EventType.Debug, "Script restarted.");
        }

        void IDebuggableScript.DebugMoveToLine(int line)
        {
            this._currentLinePointer = line;
        }

        void IDebuggableScript.DebugModifyLineContents(int line, string scriptLine)
        {
            // dangerous command...
            if ((line >= 0 && line < this.Script.Length) && !string.IsNullOrWhiteSpace(scriptLine))
            {
                this.Script[line] = scriptLine;

                ScriptDebugger.NotifyEvent(this, ScriptDebugger.EventType.Debug, "Line content modified", line.ToString(), scriptLine);
            }
            else
            {
                ScriptDebugger.NotifyEvent(this, ScriptDebugger.EventType.Debug, "Line content not modified (invalid)", line.ToString(), scriptLine);
            }
        }

        #endregion
    }
}
